Skip to content

fix(auth): honor pairing links for signed-in browsers and allow self sign-out - #261

Open
wizzoapp[bot] wants to merge 6 commits into
mainfrom
fix/pairing-auth-traps
Open

fix(auth): honor pairing links for signed-in browsers and allow self sign-out#261
wizzoapp[bot] wants to merge 6 commits into
mainfrom
fix/pairing-auth-traps

Conversation

@wizzoapp

@wizzoapp wizzoapp Bot commented Aug 20, 2026

Copy link
Copy Markdown

A signed-in browser that opened a pairing link was sent home without redeeming it. Minted links stayed unused (consumedAt stayed null), and a session without access:write had no in-product way to re-pair or sign out.

Rebased onto latest main so the diff no longer includes unrelated trailing deletions from the temp-dir teardown fix. HEAD 3650106.

Pairing links for an already-signed-in browser. Opening a same-origin one-time pairing URL is a deliberate act. /pair now keeps the credential instead of discarding it, and the apply surface requires an explicit Apply this link click before createBrowserSession replaces the current session. If the incoming grant drops scopes or narrows data access, or if either grant cannot be compared, the confirmation says so first. After a successful redeem, the previous cookie session is revoked so leftover privileged scopes cannot linger in another tab. Bearer and DPoP sessions on the same request are left alone. Pairing a different device still omits the first cookie, so both sessions remain.

Fail closed on replacement. Displacement is a security boundary, so this reverses an earlier non-blocking suggestion to swallow internal errors and continue. The replacement cookie is not installed and the handler does not report success unless the previous cookie session is revoked and its live sockets are signaled. If reading that session, interrupting its sockets, or revoking it fails, the request returns an error the UI surfaces as "Could not replace the existing session, nothing changed." The caller's original session stays intact and usable.

Desktop. The desktop app does not apply a pairing link against its local backend session, for the same reason it does not offer Sign out: the primary transport is the main-process bearer. Applying a cookie would consume the link and report success while administrative access stayed.

Downgrade is possible and intentional: the link the user applied is authoritative. The confirmation click is the safety; self sign-out is the way out if they need a different grant afterward.

Self sign-out. POST /api/auth/session/sign-out revokes the caller's own session without access:write and expires the browser cookie. Settings → Connections shows Sign out for an authenticated browser session. The desktop app does not offer Sign out for its local backend: the unbounded desktop bootstrap credential would recreate an administrative session on the next load. Minting pairing links and revoking other clients stay behind access:write. Administrative self-revoke on the existing clients/revoke endpoint remains forbidden.

Mobile. Mobile pairing is a saved-environment flow, not this cookie session. The mobile app has no self-sign-out for a saved environment yet.

Live sockets. Closing sockets on revocation is the product guarantee, not revalidation-only. Sign-out and pairing replacement both go through session revoke, which now wakes waiters for every live WebSocket bound to that session. Other tabs drop immediately, including tabs sitting on an event stream. RPC authorization also re-reads the live session so a draining connection cannot keep revoked access or previously captured scopes, including after a weaker pairing grant. Revalidation-only would have left an already-open stream signed in until its next RPC.

Grok 4.6 (T3 Code / Grok harness)

@wizzoapp

wizzoapp Bot commented Aug 20, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c4ea76108

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/server/src/auth/http.ts Outdated
Comment thread apps/server/src/auth/http.ts Outdated
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.6 KiB 11.6 KiB −5 B (−0.0%) 15.1 KiB
Codex Thread snapshot wire 5.7 KiB 5.7 KiB −9 B (−0.2%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB +4 B (+0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 51.6 KiB 51.6 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 17 17 0 (0.0%) 21
Claude Total thread wire 11.7 KiB 11.6 KiB −28 B (−0.2%) 15.1 KiB
Claude Thread snapshot wire 5.8 KiB 5.8 KiB −1 B (−0.0%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB −27 B (−0.4%) 7.8 KiB
Claude Live turn WebSocket decoded 52.4 KiB 52.4 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 17 17 0 (0.0%) 21

Baseline: 35ab477 · PR result: ff52778 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 96.9 KiB
  • Claude decoded thread snapshot: 97.6 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

wizzoapp Bot added 4 commits August 20, 2026 19:58
…sign-out

A signed-in browser that opened a pairing link was redirected home without
redeeming the credential, so minted links stayed unused. /pair now applies
the credential, replaces that browser's session with the grant's scopes, and
revokes the previous session after a successful redeem.

Replacement is fail-closed: if the existing session cannot be read or revoked,
or the replacement cookie cannot be constructed, the exchange fails, the
original session stays usable, and no replacement cookie is installed.

A session without access:write also had no way to sign itself out. Sign-out
is now self-service on this device; pairing-link minting and other-session
revoke stay behind access:write.
A signed-in browser that opened a pairing link auto-applied it, so a
standard pairing link could silently strip administrative access. Desktop
Sign out revoked the cookie then immediately signed back in from the
unbounded local bootstrap credential.

Require an explicit Apply this link click when a session already exists,
warn when that grant drops access, and keep the token so Retry works.
Hide Sign out on Desktop with a reason. Do not revoke bearer sessions when
installing a browser cookie. Pairing replacement stays fail-closed: a
failed displacement does not install a replacement cookie.

Grok 4.6 (T3 Code / Grok harness)
Sign-out and pairing replacement revoked the database session and cookie,
but already-open sockets kept the captured principal and scopes. Revocation
now interrupts those sockets, and RPC authorization re-reads the live
session so a draining connection cannot keep old permissions.

Grok 4.6 (T3 Code / Grok harness)
Pairing replacement now treats live-socket interrupt as part of
displacement. If signaling the previous cookie session's sockets fails,
the replacement cookie is not installed and the original session stays
usable.

Grok 4.6 (T3 Code / Grok harness)
@wizzoapp
wizzoapp Bot force-pushed the fix/pairing-auth-traps branch from 3258f4e to 9d0fb3a Compare August 20, 2026 19:16
@wizzoapp
wizzoapp Bot marked this pull request as draft August 20, 2026 19:17
@wizzoapp
wizzoapp Bot marked this pull request as ready for review August 20, 2026 19:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d0fb3adf3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/server/src/auth/http.ts
Comment thread apps/server/src/ws.ts
Comment thread apps/web/src/routes/pair.tsx Outdated
wizzoapp Bot added 2 commits August 20, 2026 20:28
The desktop app's primary connection is the main-process bearer. Applying a
pairing cookie there would consume the one-time link and report success
while the administrative session stayed in place. Show why instead, matching
desktop Sign out.

Grok 4.6 (T3 Code / Grok harness)
Fail-closed replacement created the new session first, so a later socket
close or revoke failure spent the one-time link, left an unreachable
session, and still told the UI that nothing changed. If revoke persisted
and MCP cleanup then failed, the browser was signed out with no cookie.

Keep creation first so the credential is the reservation against
concurrent applies. On displacement failure, revoke the new session, keep
the original cookie, and report that the link was used up. If revoke
persisted and only cleanup failed, install the replacement cookie and log
the cleanup failure. Pairing copy now matches whether the session was
kept, the link was already used, or the browser was signed out.

Grok 4.6 (T3 Code / Grok harness)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff527788c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

function* (args) {
yield* annotateEnvironmentRequest(args.endpoint.name);
const session = yield* EnvironmentAuthenticatedPrincipal;
const revoked = yield* serverAuth.revokeSession(session.sessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Complete sign-out after peer-token cleanup errors

When MCP peer-token persistence fails after SessionStore.revoke has already succeeded, revokeSession fails here before the response expires the cookie. The UI therefore reports that sign-out failed even though the browser session and sockets are already revoked, and a retry cannot clean up the still-active MCP peer credentials because the revoked cookie now receives 401; make revocation/cleanup recoverable or handle the persisted-revocation outcome before returning the error.

AGENTS.md reference: AGENTS.md:L73-L73

Useful? React with 👍 / 👎.

Comment on lines +281 to +282
const revokeError = yield* input.serverAuth.revokeSession(input.sessionId).pipe(
Effect.as(null),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize concurrent cookie replacements

When two tabs sharing the same cookie apply two different valid pairing links concurrently, both requests capture the same previous session and create a replacement before either revokes it. The first revoke returns true, but the second returns false because the old session is already revoked; Effect.as(null) discards that result, so both requests report success and install competing cookies, consuming both links and leaving one replacement session active but potentially displaced from the cookie jar. Treat a false revoke as a lost replacement race or serialize replacement against the previous session.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants